Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SPIKE] Fix announcement after picking files when using JAWS #5726

Closed
wants to merge 2 commits into from

Conversation

patrickpatrickpatrick
Copy link
Contributor

@patrickpatrickpatrick patrickpatrickpatrick commented Feb 14, 2025

After selecting a file, JAWS announces the accessible name of the button as it was before selecting. Sometimes this is the last announcement, which can make it confusing for users as the last thing they'll hear is the wrong state (potentially 'No file chosen' if it's the first time they use the component).

This PR adds an empty <span> to the component. Before the file picker opens, the focus is moved to that element (effectively with no accessible name). This doesn't have any adverse effect as the focus moves straight away to the file picker.

Upon returning on the page, we explicitely set the focus to the button so users are in the correct position on the page.

This allows JAWS + Chrome to announce the correct state for the button 🥳 Tests in NVDA + Chrome and VoiceOver + Safari sounded OK as well.

Thoughts

Button's labelling

Testing other ways to label the <button> (using the button's content or aria-label) showed that this was not an issue with our use of aria-labelledby to set the accessible name.

JAWS Announcement

JAWS is very eager to announce a ton of information when coming back to the page from the file picker:

  1. [Title of the page] - Google Chrome Unavailable (not sure why Unavailable, possibly remnant from when the file picker is open?)
  2. [Title of the page] - Google Chrome page
  3. [Title of the page] main region
  4. Upload a file, <STATUS_BEFORE_PICKING>, Chose file or drop file button, to activate press Enter
  5. Upload a file, <SELECTED_FILE_NAME>, Chose file or drop file button

Sometimes 5 gets announced at the start of the list, making it very confusing to users as the last thing they'll hear is the status of the file picker before they clicked. Even when announced at the end of the list, the name of the button is announced twice in a row, with just a slight difference in the middle 😔

Alternative solutions considered

Moving the focus explicitly, immediately

First idea was to move the focus explicitly to the button, immediately when coming back into the document.

Updating onClick to:

  onClick() {
    document.addEventListener('focusin',
      () => this.$button.focus(),
      {
        once:true
      }
    )

    this.$input.click()
  }

No change to the announcement unfortunately.

Moving the focus explicitly, delayed

Same as before, but with a 2 second delay

Updating onClick to:

  onClick() {
     document.addEventListener('focusin',
      () => setTimeout(() => this.$button.focus(), 2000),
      {
        once:true
      }
    )

    this.$input.click()
  }

No change to the announcement either.

Announcement of the status, immediately

When the status changes, make an announcement of the new status. This can be easily added by setting aria-live="polite" to the status element.

This seems to reliably add an announcement of the file name after step 4. Meaning that if 5. happens as the first thing, the last thing people hear is the name of the selected file or 'N selected files'. There's still a double announcement of the button.

Announcement of the status, debounced

Given the immediate announcement of the status already happens after JAWS re-announces the window, page, landmark you're in, I don't think it'll make a difference to delay the announcement to be debounced by a couple of seconds.

@patrickpatrickpatrick patrickpatrickpatrick changed the base branch from main to spike-enhanced-file-upload February 14, 2025 16:52
Copy link

github-actions bot commented Feb 14, 2025

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 121.49 KiB
dist/govuk-frontend-development.min.js 48.33 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 102.79 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 96.55 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.32 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 1.74 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 121.47 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 48.32 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 6.89 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 90.73 KiB 45.75 KiB
accordion.mjs 26.63 KiB 13.41 KiB
button.mjs 9.14 KiB 3.79 KiB
character-count.mjs 25.42 KiB 10.91 KiB
checkboxes.mjs 7.81 KiB 3.42 KiB
error-summary.mjs 11.04 KiB 4.55 KiB
exit-this-page.mjs 20.25 KiB 10.34 KiB
file-upload.mjs 21.47 KiB 11.34 KiB
header.mjs 6.46 KiB 3.22 KiB
notification-banner.mjs 9.4 KiB 3.71 KiB
password-input.mjs 18.21 KiB 8.34 KiB
radios.mjs 6.81 KiB 2.98 KiB
service-navigation.mjs 6.44 KiB 3.26 KiB
skip-link.mjs 6.4 KiB 2.76 KiB
tabs.mjs 12.04 KiB 6.67 KiB

View stats and visualisations on the review app


Action run for 2ce2c46

Copy link

github-actions bot commented Feb 14, 2025

JavaScript changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index f6220464f..cc295b3cc 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -779,7 +779,7 @@ class FileUpload extends ConfigurableComponent {
         const u = document.createElement("span");
         u.className = "govuk-body govuk-file-upload-button__instruction", u.innerText = this.i18n.t("dropInstruction"), l.appendChild(u), s.appendChild(l), s.setAttribute("aria-labelledby", `${i.id} ${a.id} ${s.id}`), s.addEventListener("click", this.onClick.bind(this)), s.addEventListener("dragover", (t => {
             t.preventDefault()
-        })), this.$root.insertAdjacentElement("afterbegin", s), this.$input.setAttribute("tabindex", "-1"), this.$input.setAttribute("aria-hidden", "true"), this.$button = s, this.$status = r, this.$input.addEventListener("change", this.onChange.bind(this)), this.updateDisabledState(), this.observeDisabledState(), this.$announcements = document.createElement("span"), this.$announcements.classList.add("govuk-file-upload-announcements"), this.$announcements.classList.add("govuk-visually-hidden"), this.$announcements.setAttribute("aria-live", "assertive"), this.$root.insertAdjacentElement("afterend", this.$announcements), this.$button.addEventListener("drop", this.onDrop.bind(this)), document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this)), document.addEventListener("dragenter", (() => {
+        })), this.$root.insertAdjacentElement("afterbegin", s), this.$input.setAttribute("tabindex", "-1"), this.$input.setAttribute("aria-hidden", "true"), this.$focusMagnet = document.createElement("span"), this.$focusMagnet.setAttribute("tabindex", "-1"), this.$root.appendChild(this.$focusMagnet), this.$button = s, this.$status = r, this.$input.addEventListener("change", this.onChange.bind(this)), this.updateDisabledState(), this.observeDisabledState(), this.$announcements = document.createElement("span"), this.$announcements.classList.add("govuk-file-upload-announcements"), this.$announcements.classList.add("govuk-visually-hidden"), this.$announcements.setAttribute("aria-live", "assertive"), this.$root.insertAdjacentElement("afterend", this.$announcements), this.$button.addEventListener("drop", this.onDrop.bind(this)), document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this)), document.addEventListener("dragenter", (() => {
             this.enteredAnotherElement = !0
         })), document.addEventListener("dragleave", (() => {
             this.enteredAnotherElement || this.$button.disabled || (this.hideDraggingState(), this.$announcements.innerText = this.i18n.t("leftDropZone")), this.enteredAnotherElement = !1
@@ -812,7 +812,13 @@ class FileUpload extends ConfigurableComponent {
         return t
     }
     onClick() {
-        this.$input.click()
+        this.$focusMagnet.focus({
+            preventScroll: !0
+        }), document.addEventListener("focusin", (() => this.$button.focus({
+            preventScroll: !0
+        })), {
+            once: !0
+        }), this.$input.click()
     }
     observeDisabledState() {
         new MutationObserver((t => {

Action run for 2ce2c46

Copy link

github-actions bot commented Feb 14, 2025

Other changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index b57e1a9a6..d8034bee3 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -1740,6 +1740,9 @@
       this.$root.insertAdjacentElement('afterbegin', $button);
       this.$input.setAttribute('tabindex', '-1');
       this.$input.setAttribute('aria-hidden', 'true');
+      this.$focusMagnet = document.createElement('span');
+      this.$focusMagnet.setAttribute('tabindex', '-1');
+      this.$root.appendChild(this.$focusMagnet);
       this.$button = $button;
       this.$status = $status;
       this.$input.addEventListener('change', this.onChange.bind(this));
@@ -1835,6 +1838,14 @@
       return $label;
     }
     onClick() {
+      this.$focusMagnet.focus({
+        preventScroll: true
+      });
+      document.addEventListener('focusin', () => this.$button.focus({
+        preventScroll: true
+      }), {
+        once: true
+      });
       this.$input.click();
     }
     observeDisabledState() {
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index bd59d9bb4..803a86faf 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1734,6 +1734,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.insertAdjacentElement('afterbegin', $button);
     this.$input.setAttribute('tabindex', '-1');
     this.$input.setAttribute('aria-hidden', 'true');
+    this.$focusMagnet = document.createElement('span');
+    this.$focusMagnet.setAttribute('tabindex', '-1');
+    this.$root.appendChild(this.$focusMagnet);
     this.$button = $button;
     this.$status = $status;
     this.$input.addEventListener('change', this.onChange.bind(this));
@@ -1829,6 +1832,14 @@ class FileUpload extends ConfigurableComponent {
     return $label;
   }
   onClick() {
+    this.$focusMagnet.focus({
+      preventScroll: true
+    });
+    document.addEventListener('focusin', () => this.$button.focus({
+      preventScroll: true
+    }), {
+      once: true
+    });
     this.$input.click();
   }
   observeDisabledState() {
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
index 6c37a8243..34cbe048d 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
@@ -567,6 +567,9 @@
       this.$root.insertAdjacentElement('afterbegin', $button);
       this.$input.setAttribute('tabindex', '-1');
       this.$input.setAttribute('aria-hidden', 'true');
+      this.$focusMagnet = document.createElement('span');
+      this.$focusMagnet.setAttribute('tabindex', '-1');
+      this.$root.appendChild(this.$focusMagnet);
       this.$button = $button;
       this.$status = $status;
       this.$input.addEventListener('change', this.onChange.bind(this));
@@ -662,6 +665,14 @@
       return $label;
     }
     onClick() {
+      this.$focusMagnet.focus({
+        preventScroll: true
+      });
+      document.addEventListener('focusin', () => this.$button.focus({
+        preventScroll: true
+      }), {
+        once: true
+      });
       this.$input.click();
     }
     observeDisabledState() {
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
index 307cbbd67..4d8618bc9 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
@@ -561,6 +561,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.insertAdjacentElement('afterbegin', $button);
     this.$input.setAttribute('tabindex', '-1');
     this.$input.setAttribute('aria-hidden', 'true');
+    this.$focusMagnet = document.createElement('span');
+    this.$focusMagnet.setAttribute('tabindex', '-1');
+    this.$root.appendChild(this.$focusMagnet);
     this.$button = $button;
     this.$status = $status;
     this.$input.addEventListener('change', this.onChange.bind(this));
@@ -656,6 +659,14 @@ class FileUpload extends ConfigurableComponent {
     return $label;
   }
   onClick() {
+    this.$focusMagnet.focus({
+      preventScroll: true
+    });
+    document.addEventListener('focusin', () => this.$button.focus({
+      preventScroll: true
+    }), {
+      once: true
+    });
     this.$input.click();
   }
   observeDisabledState() {
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
index 482b1798c..33be5c4d1 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
@@ -82,6 +82,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.insertAdjacentElement('afterbegin', $button);
     this.$input.setAttribute('tabindex', '-1');
     this.$input.setAttribute('aria-hidden', 'true');
+    this.$focusMagnet = document.createElement('span');
+    this.$focusMagnet.setAttribute('tabindex', '-1');
+    this.$root.appendChild(this.$focusMagnet);
     this.$button = $button;
     this.$status = $status;
     this.$input.addEventListener('change', this.onChange.bind(this));
@@ -177,6 +180,14 @@ class FileUpload extends ConfigurableComponent {
     return $label;
   }
   onClick() {
+    this.$focusMagnet.focus({
+      preventScroll: true
+    });
+    document.addEventListener('focusin', () => this.$button.focus({
+      preventScroll: true
+    }), {
+      once: true
+    });
     this.$input.click();
   }
   observeDisabledState() {

Action run for 2ce2c46

After selecting a file, JAWS announces the accessible name of the button as it was before selecting. Sometimes this is the last announcement, which can make it confusing for users as the last thing they'll hear is the wrong state (potentially 'No file chosen' if it's the first time they use the component).

Moving the focus to another element than the button before picking and explicitely focusing the button once back seems to consistently let JAWS announce the correct state of the component.
@romaricpascal romaricpascal changed the title focus fix Fix announcement after picking files when using JAWS Feb 17, 2025
@romaricpascal romaricpascal marked this pull request as ready for review February 17, 2025 17:56
This'll let us not forget that we have a fix for JAWS in place.

Testing requires us to trigger our own `focusin` event as Puppeteer does not
seem to support automatic focus/blur events: puppeteer/puppeteer#1462
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5726 February 17, 2025 18:42 Inactive
@patrickpatrickpatrick
Copy link
Contributor Author

patrickpatrickpatrick commented Feb 18, 2025

This still isn't consistently working for me (using JAWS 2025 in Chrome), if I upload two different files in quick succession, at times the first file selected is still read out last. (this is also using mouse as well, I assume a more experienced keyboard user would be able to do this faster than me if they had mistakenly chosen the wrong file)

output of the JAWS 2025 assitivlabs test
blank
To set the value use the Arrow keys or type the value.
Upload a file  ,   png-clipart-pigeon-pigeon-thumbnail.png  ,   Choose file or drop file
File upload enhanced - GOV.UK Frontend - Google Chrome Unavailable
File upload enhanced - GOV.UK Frontend - Google Chrome page
File upload enhanced - GOV.UK Frontend
MainRegion
Upload a file  ,   Basic email.csv  ,   Choose file or drop file Button
To activate press Enter.

@alphagov alphagov deleted a comment from github-actions bot Feb 18, 2025
@selfthinker
Copy link

selfthinker commented Feb 19, 2025

I've just done another round of screen reader testing and updated the testing spreadsheet accordingly.
The following three issues seem to come from this specific change (or are not fixed by this change):

  • JAWS 2025 with Firefox is the only screen reader / browser combination which consistently still reads out the old value, for example "no file chosen" after choosing a file. Although it's a significant issue, this is a combination that is more rarely used. (According to WebAIM's last screen reader survey it was 2.6%.)
  • NVDA with Firefox says "blank" when activating the button but before selecting a file. I assume that might be because of the focus on the empty span. After selecting the file it says "clickable checkbox not checked" before then saying correctly all the relevant things related to the button and the file. That might also have to do with that focus being elsewhere first. In the grand scheme of things I think this is a more minor thing. It will be confusing but not a barrier in selecting a file and understanding what's happening with it.
  • VO iOS 15.8 with Safari never reads the file name or the button, although when going via the "choose file" route it starts to say the file name and then stops itself. That might also have to do with this change. Although, before we try to do anything about that, I would wait until I get my hands on the latest iOS version and can test in there. That will be this Friday at the latest.

I couldn't replicate what @patrickpatrickpatrick reported. But it's very possible I just didn't chose the files quickly enough.

@selfthinker
Copy link

selfthinker commented Feb 21, 2025

I have tested what's announced when selecting a file in this against #5729 and #5730.
The results are in the spreadsheet in the 'different announcements' tab.

Out of those three #5729 behaves the best. As expected, JAWS with Firefox behaves nearly the same except that it also adds the file name at the end. While that is a bit confusing because it previously said (wrongly) "no file chosen" or the file name of the previously selected file, I think this is sufficient and probably the best we can do.
It also fixes the previous odd behaviour in NVDA with Firefox.
It obviously also changes what is announced in most screen readers, but I don't think the additional announcement of the file name is problematic as it still makes sense within the context.

I was also finally be able to test in the latest iOS version (18.3.1).
Unfortunately the VoiceOver behaviour got even worse, but I'm pretty sure that is entirely down to vendor bugs. That's because it also behaves weirdly, meaning interrupting itself and not announcing things, when using the parts of this user journey that are not part of the website but of the OS (like selecting the file within the file explorer). So, I don't think we need to (or can) fix anything here. Another indication of that is that it behaves the same way with the native file input element.

@romaricpascal romaricpascal marked this pull request as draft February 21, 2025 14:51
@romaricpascal romaricpascal changed the title Fix announcement after picking files when using JAWS [SPIKE] Fix announcement after picking files when using JAWS Feb 21, 2025
@romaricpascal
Copy link
Member

Closing in favour of #5729, which provides a better experience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Investigate workaround for screen readers not always announcing the selected file after picking
4 participants